﻿define(function (require, exports, module) {
    return function($){

        /*
        * jQuery Plugin: Tokenizing Autocomplete Text Entry
        * Version 1.4.2
        *
        * Copyright (c) 2009 James Smith (http://loopj.com)
        * Licensed jointly under the GPL and MIT licenses,
        * choose which one suits your project best!
        *
        */

        // Default settings
        var DEFAULT_SETTINGS = {
            hintText: "输入查找字符 ...",
            noResultsText: "未有结果",
            searchingText: "查找中 ...",
            deleteText: "&times;",
            searchDelay: 300,
            minChars: 1,
            tokenLimit: null,
            jsonContainer: null,
            method: "GET",
            contentType: "json",
            queryParam: "q",
            tokenDelimiter: ",",
            preventDuplicates: false,
            prePopulate: null,
            animateDropdown: true,
            onResult: null,
            onAdd: null,
            onDelete: null
        };

        // Default classes to use when theming
        var DEFAULT_CLASSES = {
            tokenList: "token-input-list",
            token: "token-input-token",
            tokenDelete: "token-input-delete-token",
            selectedToken: "token-input-selected-token",
            highlightedToken: "token-input-highlighted-token",
            dropdown: "token-input-dropdown",
            dropdownItem: "token-input-dropdown-item",
            dropdownItem2: "token-input-dropdown-item2",
            selectedDropdownItem: "token-input-selected-dropdown-item",
            inputToken: "token-input-input-token"
        };

        // Input box position "enum"
        var POSITION = {
            BEFORE: 0,
            AFTER: 1,
            END: 2
        };

        // Keys "enum"
        var KEY = {
            BACKSPACE: 8,
            TAB: 9,
            ENTER: 13,
            ESCAPE: 27,
            SPACE: 32,
            PAGE_UP: 33,
            PAGE_DOWN: 34,
            END: 35,
            HOME: 36,
            LEFT: 37,
            UP: 38,
            RIGHT: 39,
            DOWN: 40,
            NUMPAD_ENTER: 108,
            COMMA: 188
        };


        // Expose the .tokenInput function to jQuery as a plugin
        $.fn.tokenInput = function (url_or_data, options) {
            var settings = $.extend({}, DEFAULT_SETTINGS, options || {});

            return this.each(function () {
                new $.TokenList(this, url_or_data, settings);
            });
        };


        // TokenList class for each input
        $.TokenList = function (input, url_or_data, settings) {
            //
            // Initialization
            //

            // Configure the data source
            if ($.type(url_or_data) === "string") {
                // Set the url to query against
                settings.url = url_or_data;

                // Make a smart guess about cross-domain if it wasn't explicitly specified
                if (settings.crossDomain === undefined) {
                    if (settings.url.indexOf("://") === -1) {
                        settings.crossDomain = false;
                    } else {
                        settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
                    }
                }
            } else if ($.type(url_or_data) === "array") {
                // Set the local data to search through
                settings.local_data = url_or_data;
            }

            // Build class names
            if (settings.classes) {
                // Use custom class names
                settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
            } else if (settings.theme) {
                // Use theme-suffixed default class names
                settings.classes = {};
                $.each(DEFAULT_CLASSES, function (key, value) {
                    settings.classes[key] = value + "-" + settings.theme;
                });
            } else {
                settings.classes = DEFAULT_CLASSES;
            }


            // Save the tokens
            var saved_tokens = [];

            // Keep track of the number of tokens in the list
            var token_count = 0;

            // Basic cache to save on db hits
            var cache = new $.TokenList.Cache();

            // Keep track of the timeout, old vals
            var timeout;
            var input_val;

            // Create a new text input an attach keyup events
            var input_box = $("<input type=\"text\"  autocomplete=\"off\">")
        .css({
            outline: "none"
        })
        .focus(function () {
            if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
                show_dropdown_hint();
            }
        })
        .blur(function () {
            hide_dropdown();
        })
        .bind("keyup keydown blur update", resize_input)
        .keydown(function (event) {
            var previous_token;
            var next_token;

            switch (event.keyCode) {
                case KEY.LEFT:
                case KEY.RIGHT:
                case KEY.UP:
                case KEY.DOWN:
                    if (!$(this).val()) {
                        previous_token = input_token.prev();
                        next_token = input_token.next();

                        if ((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
                            // Check if there is a previous/next token and it is selected
                            if (event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
                                deselect_token($(selected_token), POSITION.BEFORE);
                            } else {
                                deselect_token($(selected_token), POSITION.AFTER);
                            }
                        } else if ((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
                            // We are moving left, select the previous token if it exists
                            select_token($(previous_token.get(0)));
                        } else if ((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
                            // We are moving right, select the next token if it exists
                            select_token($(next_token.get(0)));
                        }
                    } else {
                        var dropdown_item = null;

                        if (event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
                            dropdown_item = $(selected_dropdown_item).next();
                        } else {
                            dropdown_item = $(selected_dropdown_item).prev();
                        }

                        if (dropdown_item.length) {
                            select_dropdown_item(dropdown_item);
                        }
                        return false;
                    }
                    break;

                case KEY.BACKSPACE:
                    previous_token = input_token.prev();

                    if (!$(this).val().length) {
                        if (selected_token) {
                            delete_token($(selected_token));
                        } else if (previous_token.length) {
                            select_token($(previous_token.get(0)));
                        }

                        return false;
                    } else if ($(this).val().length === 1) {
                        hide_dropdown();
                    } else {
                        // set a timeout just long enough to let this function finish.
                        setTimeout(function () { do_search(); }, 5);
                    }
                    break;

                case KEY.TAB:
                case KEY.ENTER:
                case KEY.NUMPAD_ENTER:
                case KEY.COMMA:
                    if (selected_dropdown_item) {
                        add_token($(selected_dropdown_item));
                        return false;
                    }
                    break;

                case KEY.ESCAPE:
                    hide_dropdown();
                    return true;

                default:
                    if (String.fromCharCode(event.which)) {
                        // set a timeout just long enough to let this function finish.
                        setTimeout(function () { do_search(); }, 5);
                    }
                    break;
            }
        });

            // Keep a reference to the original input box
            var hidden_input = $(input)
                           .hide()
                           .val("")
                           .focus(function () {
                               input_box.focus();
                           })
                           .blur(function () {
                               input_box.blur();
                           });

            // Keep a reference to the selected token and dropdown item
            var selected_token = null;
            var selected_dropdown_item = null;

            // The list to store the token items in
            var token_list = $("<ul />")
        .addClass(settings.classes.tokenList)
        .click(function (event) {
            var li = $(event.target).closest("li");
            if (li && li.get(0) && $.data(li.get(0), "tokeninput")) {
                toggle_select_token(li);
            } else {
                // Deselect selected token
                if (selected_token) {
                    deselect_token($(selected_token), POSITION.END);
                }

                // Focus input box
                input_box.focus();
            }
        })
        .mouseover(function (event) {
            var li = $(event.target).closest("li");
            if (li && selected_token !== this) {
                li.addClass(settings.classes.highlightedToken);
            }
        })
        .mouseout(function (event) {
            var li = $(event.target).closest("li");
            if (li && selected_token !== this) {
                li.removeClass(settings.classes.highlightedToken);
            }
        })
        .insertBefore(hidden_input);

            // The token holding the input box
            var input_token = $("<li />")
        .addClass(settings.classes.inputToken)
        .appendTo(token_list)
        .append(input_box);

            // The list to store the dropdown items in
            var dropdown = $("<div>")
        .addClass(settings.classes.dropdown)
        .appendTo("body")
        .hide();

            // Magic element to help us resize the text input
            var input_resizer = $("<tester/>")
        .insertAfter(input_box)
        .css({
            position: "absolute",
            top: -9999,
            left: -9999,
            width: "auto",
            fontSize: input_box.css("fontSize"),
            fontFamily: input_box.css("fontFamily"),
            fontWeight: input_box.css("fontWeight"),
            letterSpacing: input_box.css("letterSpacing"),
            whiteSpace: "nowrap"
        });

            // Pre-populate list if items exist
            hidden_input.val("");
            li_data = settings.prePopulate || hidden_input.data("pre");
            if (li_data && li_data.length) {
                $.each(li_data, function (index, value) {
                    insert_token(value.id, value.name);
                });
            }



            //
            // Private functions
            //

            function resize_input() {
                if (input_val === (input_val = input_box.val())) { return; }

                // Enter new content into resizer and resize input accordingly
                var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g, ' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
                input_resizer.html(escaped);
                input_box.width(input_resizer.width() + 30);
            }

            function is_printable_character(keycode) {
                return ((keycode >= 48 && keycode <= 90) ||     // 0-1a-z
                (keycode >= 96 && keycode <= 111) ||    // numpad 0-9 + - / * .
                (keycode >= 186 && keycode <= 192) ||   // ; = , - . / ^
                (keycode >= 219 && keycode <= 222));    // ( \ ) '
            }

            // Inner function to a token to the list
            function insert_token(id, value) {
                var this_token = $("<li><p>" + value + "</p> </li>")
          .addClass(settings.classes.token)
          .insertBefore(input_token);

                // The 'delete token' button
                $("<span>" + settings.deleteText + "</span>")
            .addClass(settings.classes.tokenDelete)
            .appendTo(this_token)
            .click(function () {
                delete_token($(this).parent());
                return false;
            });

                // Store data on the token
                var token_data = { "id": id, "name": value };
                $.data(this_token.get(0), "tokeninput", token_data);

                // Save this token for duplicate checking
                saved_tokens.push(token_data);

                // Update the hidden input
                var token_ids = $.map(saved_tokens, function (el) {
                    return el.id;
                });
                hidden_input.val(token_ids.join(settings.tokenDelimiter));

                token_count += 1;

                return this_token;
            }

            // Add a token to the token list based on user input
            function add_token(item) {
                var li_data = $.data(item.get(0), "tokeninput");
                var callback = settings.onAdd;

                // See if the token already exists and select it if we don't want duplicates
                if (token_count > 0 && settings.preventDuplicates) {
                    var found_existing_token = null;
                    token_list.children().each(function () {
                        var existing_token = $(this);
                        var existing_data = $.data(existing_token.get(0), "tokeninput");
                        if (existing_data && existing_data.id === li_data.id) {
                            found_existing_token = existing_token;
                            return false;
                        }
                    });

                    if (found_existing_token) {
                        select_token(found_existing_token);
                        input_token.insertAfter(found_existing_token);
                        input_box.focus();
                        return;
                    }
                }

                // Insert the new tokens
                insert_token(li_data.id, li_data.name);

                // Check the token limit
                if (settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
                    input_box.hide();
                    hide_dropdown();
                    return;
                } else {
                    input_box.focus();
                }

                // Clear input box
                input_box.val("");

                // Don't show the help dropdown, they've got the idea
                hide_dropdown();

                // Execute the onAdd callback if defined
                if ($.isFunction(callback)) {
                    callback(li_data);
                }
            }

            // Select a token in the token list
            function select_token(token) {
                token.addClass(settings.classes.selectedToken);
                selected_token = token.get(0);

                // Hide input box
                input_box.val("");

                // Hide dropdown if it is visible (eg if we clicked to select token)
                hide_dropdown();
            }

            // Deselect a token in the token list
            function deselect_token(token, position) {
                token.removeClass(settings.classes.selectedToken);
                selected_token = null;

                if (position === POSITION.BEFORE) {
                    input_token.insertBefore(token);
                } else if (position === POSITION.AFTER) {
                    input_token.insertAfter(token);
                } else {
                    input_token.appendTo(token_list);
                }

                // Show the input box and give it focus again
                input_box.focus();
            }

            // Toggle selection of a token in the token list
            function toggle_select_token(token) {
                var previous_selected_token = selected_token;

                if (selected_token) {
                    deselect_token($(selected_token), POSITION.END);
                }

                if (previous_selected_token === token.get(0)) {
                    deselect_token(token, POSITION.END);
                } else {
                    select_token(token);
                }
            }

            // Delete a token from the token list
            function delete_token(token) {
                // Remove the id from the saved list
                var token_data = $.data(token.get(0), "tokeninput");
                var callback = settings.onDelete;

                // Delete the token
                token.remove();
                selected_token = null;

                // Show the input box and give it focus again
                input_box.focus();

                // Remove this token from the saved list
                saved_tokens = $.grep(saved_tokens, function (val) {
                    return (val.id !== token_data.id);
                });

                // Update the hidden input
                var token_ids = $.map(saved_tokens, function (el) {
                    return el.id;
                });
                hidden_input.val(token_ids.join(settings.tokenDelimiter));

                token_count -= 1;

                if (settings.tokenLimit !== null) {
                    input_box
                .show()
                .val("")
                .focus();
                }

                // Execute the onDelete callback if defined
                if ($.isFunction(callback)) {
                    callback(token_data);
                }
            }

            // Hide and clear the results dropdown
            function hide_dropdown() {
                dropdown.hide().empty();
                selected_dropdown_item = null;
            }

            function show_dropdown() {
                dropdown
            .css({
                position: "absolute",
                top: $(token_list).offset().top + $(token_list).outerHeight(),
                left: $(token_list).offset().left,
                zindex: 999
            })
            .show();
            }

            function show_dropdown_searching() {
                if (settings.searchingText) {
                    dropdown.html("<p>" + settings.searchingText + "</p>");
                    show_dropdown();
                }
            }

            function show_dropdown_hint() {
                if (settings.hintText) {
                    dropdown.html("<p>" + settings.hintText + "</p>");
                    show_dropdown();
                }
            }

            // Highlight the query part of the search term
            function highlight_term(value, term) {
                return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
            }

            // Populate the results dropdown with some results
            function populate_dropdown(query, results) {
                if (results && results.length) {
                    dropdown.empty();
                    var dropdown_ul = $("<ul>")
                .appendTo(dropdown)
                .mouseover(function (event) {
                    select_dropdown_item($(event.target).closest("li"));
                })
                .mousedown(function (event) {
                    add_token($(event.target).closest("li"));
                    return false;
                })
                .hide();

                    $.each(results, function (index, value) {
                        var this_li = $("<li>" + highlight_term(value.name, query) + "</li>")
                                  .appendTo(dropdown_ul);

                        if (index % 2) {
                            this_li.addClass(settings.classes.dropdownItem);
                        } else {
                            this_li.addClass(settings.classes.dropdownItem2);
                        }

                        if (index === 0) {
                            select_dropdown_item(this_li);
                        }

                        $.data(this_li.get(0), "tokeninput", { "id": value.id, "name": value.name });
                    });

                    show_dropdown();

                    if (settings.animateDropdown) {
                        dropdown_ul.slideDown("fast");
                    } else {
                        dropdown_ul.show();
                    }
                } else {
                    if (settings.noResultsText) {
                        dropdown.html("<p>" + settings.noResultsText + "</p>");
                        show_dropdown();
                    }
                }
            }

            // Highlight an item in the results dropdown
            function select_dropdown_item(item) {
                if (item) {
                    if (selected_dropdown_item) {
                        deselect_dropdown_item($(selected_dropdown_item));
                    }

                    item.addClass(settings.classes.selectedDropdownItem);
                    selected_dropdown_item = item.get(0);
                }
            }

            // Remove highlighting from an item in the results dropdown
            function deselect_dropdown_item(item) {
                item.removeClass(settings.classes.selectedDropdownItem);
                selected_dropdown_item = null;
            }

            // Do a search and show the "searching" dropdown if the input is longer
            // than settings.minChars
            function do_search() {
                var query = input_box.val().toLowerCase();

                if (query && query.length) {
                    if (selected_token) {
                        deselect_token($(selected_token), POSITION.AFTER);
                    }

                    if (query.length >= settings.minChars) {
                        show_dropdown_searching();
                        clearTimeout(timeout);

                        timeout = setTimeout(function () {
                            run_search(query);
                        }, settings.searchDelay);
                    } else {
                        hide_dropdown();
                    }
                }
            }

            // Do the actual search
            function run_search(query) {
                var cached_results = cache.get(query);
                if (cached_results) {
                    populate_dropdown(query, cached_results);
                } else {
                    // Are we doing an ajax search or local data search?
                    if (settings.url) {
                        // Extract exisiting get params
                        var ajax_params = {};
                        ajax_params.data = {};
                        if (settings.url.indexOf("?") > -1) {
                            var parts = settings.url.split("?");
                            ajax_params.url = parts[0];

                            var param_array = parts[1].split("&");
                            $.each(param_array, function (index, value) {
                                var kv = value.split("=");
                                ajax_params.data[kv[0]] = kv[1];
                            });
                        } else {
                            ajax_params.url = settings.url;
                        }

                        // Prepare the request
                        ajax_params.data[settings.queryParam] = query;
                        ajax_params.type = settings.method;
                        ajax_params.dataType = settings.contentType;
                        if (settings.crossDomain) {
                            ajax_params.dataType = "jsonp";
                        }

                        // Attach the success callback
                        ajax_params.success = function (results) {
                            if ($.isFunction(settings.onResult)) {
                                results = settings.onResult.call(this, results);
                            }
                            cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);

                            // only populate the dropdown if the results are associated with the active search query
                            if (input_box.val().toLowerCase() === query) {
                                populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
                            }
                        };

                        // Make the request
                        $.ajax(ajax_params);
                    } else if (settings.local_data) {
                        // Do the search through local data
                        var results = $.grep(settings.local_data, function (row) {
                            return row.value.toLowerCase().indexOf(query.toLowerCase()) > -1;
                        });

                        populate_dropdown(query, results);
                    }
                }
            }
        };

        // Really basic cache for the results
        $.TokenList.Cache = function (options) {
            var settings = $.extend({
                max_size: 500
            }, options);

            var data = {};
            var size = 0;

            var flush = function () {
                data = {};
                size = 0;
            };

            this.add = function (query, results) {
                if (size > settings.max_size) {
                    flush();
                }

                if (!data[query]) {
                    size += 1;
                }

                data[query] = results;
            };

            this.get = function (query) {
                return data[query];
            };
        };
    }

});